Go互斥锁实现原理 | 您所在的位置:网站首页 › go lock › Go互斥锁实现原理 |
Go语言中的锁简单易用,本文整理一下锁的实现原理。 Golang中锁有两种,互斥锁Mutex和读写互斥锁RWMutex,互斥锁也叫读锁,读写锁也叫读锁,相互之间的关系为: 写锁需要阻塞写锁:一个协程拥有写锁时,其他协程写锁定需要阻塞 写锁需要阻塞读锁:一个协程拥有写锁时,其他协程读锁定需要阻塞 读锁需要阻塞写锁:一个协程拥有读锁时,其他协程写锁定需要阻塞 读锁不能阻塞读锁:一个协程拥有读锁时,其他协程也可以拥有读锁 1.使用互斥锁和读写锁在使用上没有很大区别 互斥锁使用Lock()进行加锁,使用Unlock()进行解锁 读写锁使用RLock()加读锁,使用RUnlock()进行解读锁;使用Lock()加写锁,使用Unlock解写锁,和互斥锁功能一致; 但两者使用场景不同: 互斥锁会将操作串行化,可以保证操作完全有序,适合资源只能由一个协程进行操作的情况,并发能力弱; 读写锁适合读多写少的情况,并发能有比较强。 package main import ( "fmt" "sync" "time" ) /** * @Author: Jason Pang * @Description: 测试互斥锁 */ func testMutex() { count := 0 var l sync.Mutex for i := 0; i < 10; i++ { go func() { l.Lock() defer l.Unlock() fmt.Println("---------互斥锁", count) count++ }() } } /** * @Author: Jason Pang * @Description: 测试读写锁 */ func testRWMutex() { count := 0 var l sync.RWMutex for i := 0; i < 10; i++ { go func() { l.RLock() defer l.RUnlock() fmt.Println("---------读写互斥锁", count) count++ }() } } func main() { testMutex() testRWMutex() time.Sleep(10 * time.Second) }输出:
讲锁的具体实现原理之前,需要先复习几个基础知识:进程同步、信号量和自旋。 2.1进程同步进程同步本质上是靠控制对临界区的访问权限实现的。 临界资源:把在一段时间内只允许一个进程访问的资源称为临界资源或独占资源。计算机系统中的大多数物理设备,以及某些软件中所用的栈、变量和表格,都属于临界资源, 它们要求被互斥地共享 临界区:在每个进程中访问临界资源的那段代码称为临界区(critical section)。若能保证诸进程互斥地进入自己的临界区,便可实现诸进程对临界资源的互斥访问。 进入区(entry section):如果此刻该临界资源未被访问,进程便可进入临界区对该资源进行访问,并设置它正被访问的标志;如果此刻该临界资源正被某进程访问,则本进程不能进入临界区。 退出区(exit section):用于将临界区正被访问的标志恢复为未被访问的标志。 同步机制规则 (1) 空闲让进。当无进程处于临界区时,表明临界资源处于空闲状态,应允许一个请求进入临界区的进程立即进入自己的临界区,以有效地利用临界资源。 (2) 忙则等待。当已有进程进入临界区时,表明临界资源正在被访问,因而其它试图进 入临界区的进程必须等待,以保证对临界资源的互斥访问。 (3) 有限等待。对要求访问临界资源的进程,应保证在有限时间内能进入自己的临界区, 以免陷入“死等”状态。 (4) 让权等待。当进程不能进入自己的临界区时,应立即释放处理机,以免进程陷入“忙等”状态。 这个规则和现实一致:如果有空闲我就可以用吧(空闲让进);如果不空闲,为了有序我可以等待(忙则等待);我等待的时候没别的事情可以做,那可以去一边休息吧(让权等待);你们不能让我老等着吧(有限等待); 2.2信号量1965 年,荷兰学者 Dijkstra 提出的信号量(Semaphores)机制是一种卓有成效的进程同步工具。Dijkstra,YYDS。 2.2.1类型信号量现在已发展为如下四种类型: 整型信号量 记录型信号量 AND型信号量 信号量集 虽然信号量有不同类型,但核心是对:一个用于表示资源数目的整型量 S,仅能通过两个标准的原子操作(Atomic Operation) wait(S)和 signal(S)来访问。wait用于将S值变小,signal用于将S值增加,伪代码如下: wait(S):while S // Fast path: grab unlocked mutex. // 如果锁即没被占用、也不是饥饿状态、也没有唤醒协程、也没有等待的协程,直接加锁成功 // 这是比较完美的一种状态 if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { if race.Enabled { //默认是false,所以可以不用管 race.Acquire(unsafe.Pointer(m)) } return } // Slow path (outlined so that the fast path can be inlined) m.lockSlow() } func (m *Mutex) lockSlow() { var waitStartTime int64 starving := false awoke := false iter := 0 old := m.state for { // Don't spin in starvation mode, ownership is handed off to waiters // so we won't be able to acquire the mutex anyway. // 如果是正常模式且锁被抢占了,自己符合自旋条件,就自旋 // 因为按照规定,饥饿模式下需要保证等待队列中的协程能够获得锁的所有权,防止等待队列饿死 // 如果锁变为饥饿状态或者已经解锁了,或者不符合自旋条件了就结束自旋 if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) { // Active spinning makes sense. // Try to set mutexWoken flag to inform Unlock // to not wake other blocked goroutines. // 如果等待队列有协程、锁没有设置唤醒状态,就努力设置唤醒状态 // 这么做的好处是,当锁解锁的时候,不会去唤醒已经阻塞的协程,保证自己更大概率获取到锁 if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 && atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) { awoke = true } runtime_doSpin() iter++ old = m.state continue } // 此处说明锁变为饥饿状态或者已经解锁了,或者不符合自旋条件了(仍为锁定状态) // 锁状态包含-饥饿锁定、饥饿未锁定、正常锁定、正常未锁定 // 获取锁最新的状态 new := old // Don't try to acquire starving mutex, new arriving goroutines must queue. // 如果是正常状态,尝试加锁。饥饿状态下要出让竞争权利,肯定不能加锁的 if old&mutexStarving == 0 { new |= mutexLocked } // 如果锁还是被占用的或者锁是饥饿状态,只能将自己放到等待队列上 // 到了这个阶段,遇到这些状态,协程只能躺平。饥饿状态要出让竞争权利 if old&(mutexLocked|mutexStarving) != 0 { new += 1 |
CopyRight 2018-2019 实验室设备网 版权所有 |